﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Xml;
using System.IO;
using System.Security.Cryptography;
using Axiom.Graphics;
using Axiom.Core;

namespace GameDesigner
{
    public class ResourceList
    {
        [EditorAttribute(typeof(GameDesigner.TypeEditors.ResourceListTypeEditor), typeof(System.Drawing.Design.UITypeEditor))]
        public List<Resource> Resources { get; set; }
        public DirectoryInfo RootFolder { get; set; }
        public int DefaultMipMapCount { get; set; }

        public ResourceList(List<Resource> Resources, DirectoryInfo rootFolder)
        {
            this.Resources = Resources;
            this.DefaultMipMapCount = 5;
            this.RootFolder = rootFolder;
        }

        public List<Resource> FindFileInResources(string fileAddress)
        {
            List<Resource> resWithFile = new List<Resource>();

            string SourceMD5 = GetMD5HashFromFile(fileAddress);
            string fileName = Path.GetFileName(fileAddress);

            //find all matching file names and test if their md5 is the same of source
            foreach (Resource res in Resources)
            {
                string[] foundFiles = Directory.GetFiles(
                                            res.Location,
                                            fileName,
                                           res.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);

                if (foundFiles.Length > 0)
                {
                    for (int i = 0; i < foundFiles.Length; i++)
                        if (GetMD5HashFromFile(foundFiles[i]) == SourceMD5)
                            resWithFile.Add(res);
                }
            }

            return resWithFile;
        }

        public bool IsFileInResources(string fileAddress)
        {
            if (FindFileInResources(fileAddress).Count > 0)
                return true;
            else
                return false;
        }

        /// <summary>
        /// Make a local copy of input resource and adds it into the Resource list
        /// </summary>
        /// <param name="res"></param>
        public void AddResource(Resource res)
        {
            DirectoryInfo destDir;
            FileInfo sourceFile;
            DirectoryInfo sourceDir;

            string Origin = res.Location;

            //location of resources must countain resources name too
            res.Location = RootFolder.FullName + Path.DirectorySeparatorChar + res.Group + Path.DirectorySeparatorChar + Path.GetFileName(Origin);

            if (!String.IsNullOrEmpty(Origin))
            {
                destDir = new DirectoryInfo(
                            res.Type == "Folder" ?
                            res.Location :
                            Path.GetDirectoryName(res.Location)
                    );

                sourceDir = new DirectoryInfo(Origin);
                sourceFile = new FileInfo(Origin);

                //check for disk space
                System.Diagnostics.Debug.Write("Calculating the needed disk space for resource: " + res.ToString());
                long neededSpace = 0;
                if (res.Type.Equals("Folder"))
                    neededSpace = GetDirectorySize(sourceDir, res.Recursive);
                else
                    neededSpace = sourceFile.Length;

                DriveInfo destDrive = new DriveInfo(destDir.FullName);

                if (destDrive.TotalFreeSpace < neededSpace)
                {
                    throw new Exception(
                        "Not enough space for resource \"" + res.ToString() +
                        "\nNeeded: " + neededSpace.ToString() +
                        "\nAvailable: " + destDrive.TotalFreeSpace.ToString());
                }

                System.Diagnostics.Debug.Write("Consuming " + (neededSpace / 1024).ToString() + "kb in project root.");

                //create destination directory and copy files
                System.Diagnostics.Debug.Write("Start copying files.");

                //rename to copy dir files
                if (res.Type.Equals("Folder"))
                    copyDirectory(sourceDir, destDir, res.Recursive);
                else
                    copyFile(sourceFile, destDir);

                System.Diagnostics.Debug.Write("Copy finished.");
            }

            Resources.Add(res);
        }

        public void AddFileToGeneral(string fileAddress)
        {
            copyFile(new FileInfo(fileAddress), new DirectoryInfo(GetGeneralResource().Location));
        }

        public void RemoveFileFromGeneral(string fileAddress)
        {
            string fileNameInRes = GetGeneralResource().Location + Path.DirectorySeparatorChar + Path.GetFileName(fileAddress);

            if (File.Exists(fileNameInRes))
                File.Delete(fileNameInRes);
        }

        public void RemoveResource(Resource res)
        {
            if (Resources.Remove(res))
            {
                if (res.Type == "Folder")
                    Directory.Delete(res.Location, true);
                else if (res.Type == "ZipFile")
                    File.Delete(res.Location);

                //remove group folder if there is no other resource in this group
                bool emptyResGroup = true;
                foreach (Resource r in Resources)
                    if (r.Group == res.Group)
                        emptyResGroup = false;

                if (emptyResGroup)
                    Directory.Delete(Path.GetDirectoryName(res.Location));
            }
        }

        public Resource GetGeneralResource()
        {
            foreach (Resource res in Resources)
                if (res.Location.EndsWith(Path.DirectorySeparatorChar + "General"))
                    return res;

            //if general resource group does not exists
            Resource GenRes = new Resource(
                    "",
                    "Folder", "General", false);

            AddResource(GenRes);

            return GenRes;
        }

        protected void copyFile(FileInfo file, DirectoryInfo destDir)
        {
            if (!destDir.Exists)
                Directory.CreateDirectory(destDir.FullName);

            file.CopyTo(destDir.FullName + Path.DirectorySeparatorChar + file.Name, true);
        }

        protected void copyDirectory(DirectoryInfo sourceDir, DirectoryInfo destDir, bool Recursive)
        {
            if (!destDir.Exists)
                Directory.CreateDirectory(destDir.FullName);

            foreach (FileInfo file in sourceDir.GetFiles())
                file.CopyTo(destDir.FullName + Path.DirectorySeparatorChar + file.Name, true);

            if (Recursive)
                foreach (DirectoryInfo dir in sourceDir.GetDirectories())
                    copyDirectory(
                        dir,
                        new DirectoryInfo(destDir.FullName + Path.DirectorySeparatorChar + dir.Name),
                        Recursive
                        );
        }

        protected long GetDirectorySize(DirectoryInfo destDir, bool Recursive)
        {
            long sum = 0;

            //sum up all files
            foreach (FileInfo file in destDir.GetFiles())
                sum += file.Length;

            //sum up all subfolders if recursive
            if (Recursive)
                foreach (DirectoryInfo subdir in destDir.GetDirectories())
                    sum += GetDirectorySize(subdir, Recursive);

            return sum;
        }

        protected static string GetMD5HashFromFile(string fileName)
        {
            FileStream file = new FileStream(fileName, FileMode.Open);
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] retVal = md5.ComputeHash(file);
            file.Close();

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < retVal.Length; i++)
            {
                sb.Append(retVal[i].ToString("x2"));
            }
            return sb.ToString();
        }
    }

    /// <summary>
    /// This class is used to present a resource in the list.
    /// </summary>
    [DefaultPropertyAttribute("Location")]
    public class Resource
    {
        private string _location;
        [EditorAttribute(typeof(GameDesigner.TypeEditors.ResourceLocationTypeEditor), typeof(System.Drawing.Design.UITypeEditor))]
        [DescriptionAttribute("Path to the resource. It most be a valid path and will be translated to reletive to the project root.")]
        public string Location
        {
            get
            {
                return _location;
            }

            set
            {
                value = value.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString()) ?
                    value.Remove(value.Length - 1) :
                    value;

                if (ResourceChanged != null)
                {
                    Resource OldResource = new Resource(this);
                    _location = value;
                    ResourceChanged(this, OldResource);
                }
                else
                    _location = value;
            }
        }

        private string _type;
        [TypeConverter(typeof(GameDesigner.TypeConverters.ResourceTypeTypeConverter))]
        public string Type
        {
            get
            {
                return _type;
            }

            set
            {
                if (ResourceChanged != null)
                {
                    Resource OldResource = new Resource(this);
                    _type = value;
                    ResourceChanged(this, OldResource);
                }
                _type = value;
            }
        }

        private string _group;
        [TypeConverter(typeof(GameDesigner.TypeConverters.ResourceGroupsTypeConverter))]
        public string Group
        {
            get
            {
                return _group;
            }

            set
            {
                if (ResourceChanged != null)
                {
                    Resource OldResource = new Resource(this);
                    _group = value;
                    ResourceChanged(this, OldResource);
                }
                else
                    _group = value;
            }
        }

        private bool _recursive;
        [DescriptionAttribute("If true will search subdirectories for the resources, however these most be addressed reletively to be used in program.")]
        public bool Recursive
        {
            get
            {
                return _recursive;
            }

            set
            {
                if (ResourceChanged != null)
                {
                    Resource OldResource = new Resource(this);
                    _recursive = value;
                    ResourceChanged(this, OldResource);
                }
                else
                    _recursive = value;
            }
        }

        public delegate void ResourceChangedDelegate(Resource NewResource, Resource OldResource);

        public event ResourceChangedDelegate ResourceChanged;

        public Resource(string Location)
            : this(Location, "Folder", "General", false)
        { }

        public Resource(Resource resource)
            : this(resource.Location, resource.Type, resource.Group, resource.Recursive)
        { }

        public Resource(string Location, string Type, string Group, bool Recursive)
        {
            this.Location = Location;
            this.Type = Type;
            this.Group = Group;
            this.Recursive = Recursive;
        }

        public override string ToString()
        {
            return Location + " ; " + Type + " ; " + Group + " ; Recursive:" + Recursive.ToString();
        }
    }

    public class TerrainSceneManagerTerrainConfig
    {
        #region Properties
        [EditorAttribute(typeof(GameDesigner.TypeEditors.OpenFileTypeEditor), typeof(System.Drawing.Design.UITypeEditor))]
        [DescriptionAttribute("The image file from which the heightmap is drawn.Must be a square where each dimension is of size 2^n+1, for some integer n.")]
        public string Terrain { set; get; }

        [EditorAttribute(typeof(GameDesigner.TypeEditors.OpenFileTypeEditor), typeof(System.Drawing.Design.UITypeEditor))]
        [DescriptionAttribute("Specifies the name of the terrain texture.")]
        public string WorldTexture { set; get; }

        [EditorAttribute(typeof(GameDesigner.TypeEditors.OpenFileTypeEditor), typeof(System.Drawing.Design.UITypeEditor))]
        [DescriptionAttribute("Specifies the name of the detail texture.")]
        public string DetailTexture { set; get; }

        //validated to be bigger than 0
        [DescriptionAttribute("This specifies the number of times that the detail texture will be repeated in each terrain tile.If this number is low, the terrain may appear blurry at close distances. If this number is high, the terrain may appear to have repeating patterns when viewed over a distance.")]
        public int DetailTile { set; get; }

        [DescriptionAttribute("Terrain tiles have the dimension TileSize x TileSize vertices. This number must be smaller than PageSize. TileSize must have a value 2^n+1 for some integer n.")]
        public int TileSize { set; get; }

        [DescriptionAttribute("The terrain will be PageSize x PageSize vertices large. PageSize must have the same value as the dimension of the heightmap image, and so must also have a value 2^n+1 for some integer n.")]
        public long PageSize { set; get; }

        [DescriptionAttribute("The maximum height of the terrain in world coordinates. The 0..255 (resp. 0..65535) range from the heightmap is scaled to 0..MaxHeight in world coordinates.")]
        public int MaxHeight { set; get; }

        [DescriptionAttribute("This specifies the maximum error tolerated when determining which level of detail to use. This is the main way to control the distance at which the terrain displays LODs, it is not a distance in world units but rather how much error can appear on the screen.")]
        public int MaxPixelError { set; get; }

        [DescriptionAttribute("Specifies the number of levels of detail that will be used in rendering the terrain. ")]
        public int MaxMipMapLevel { set; get; }

        [DescriptionAttribute("This sets the extents of the terrain in world coordinates. This can be used to scale the terrain to any size you want.")]
        public long PageWorldX { set; get; }

        [DescriptionAttribute("This sets the extents of the terrain in world coordinates. This can be used to scale the terrain to any size you want.")]
        public long PageWorldZ { set; get; }

        //values limited to yes and no
        [TypeConverter(typeof(GameDesigner.TypeConverters.YesNoValueTypeConverter))]
        [DescriptionAttribute("This makes the TerrainSceneManager calculate and set the vertex colors in the hardware buffer. Turn this on if you use a GPU program that needs this information.")]
        public string VertexColours { set; get; }

        [TypeConverter(typeof(GameDesigner.TypeConverters.YesNoValueTypeConverter))]
        [DescriptionAttribute("When set to yes, optimizes the order in which terrain triangles are sent to the GPU so that fewer vertices are sent, it is recommended that you turn this off.")]
        public string UseTriStrips { set; get; }

        [TypeConverter(typeof(GameDesigner.TypeConverters.YesNoValueTypeConverter))]
        [DescriptionAttribute("Use vertex program to morph LODs, if available.")]
        public string VertexProgramMorph { set; get; }

        [TypeConverter(typeof(GameDesigner.TypeConverters.YesNoValueTypeConverter))]
        [DescriptionAttribute("This makes the TerrainSceneManager calculate and set vertex normals in the hardware buffer. If you use lighting or a GPU program that requires it, you should turn this on.")]
        public string VertexNormals { set; get; }

        [DescriptionAttribute("The proportional distance range at which the LOD morph starts to take effect. This is as a proportion of the distance between the current LODs effective range, and the effective range of the next lower LOD.")]
        public float LODMorphStart { set; get; }

        [DescriptionAttribute("")]
        public float ScaleX { set; get; }

        [DescriptionAttribute("")]
        public float ScaleY { set; get; }

        [DescriptionAttribute("")]
        public float ScaleZ { set; get; }

        //TODO: may have a type editor that limits the values to available values
        [DescriptionAttribute("The name of the material you will define to shade the terrain.")]
        public string MaterialName { set; get; }

        [DescriptionAttribute("This option specifies the name of the vertex program parameter to which you wish to bind the morph LOD. This parameter is 0 when there is no adjustment (highest) to 1 when the morph takes it completely to the same position as the next lower LOD.This option is used if VertexProgramMorph is set to yes and your custom material includes a high-level vertex program.")]
        public string MorphLODFactorParamName { set; get; }

        //most be limited between 0 and 1
        [DescriptionAttribute("This option represents the index of the vertex program parameter to which you wish to bind the morph LOD factor. This is 0 when there is no adjustment (highest) to 1 when the morph takes it completely to the same position as the next lower LOD.This option is used if VertexProgramMorph is set to yes and your custom material includes an assemler-level vertex program.")]
        public float MorphLODFactorParamIndex { set; get; }

        [DescriptionAttribute("Specifies the source of the heightmap. By default, Heightmap.")]
        public string PageSource { set; get; }
        #endregion

        public ResourceList ResourceList;

        public TerrainSceneManagerTerrainConfig(ResourceList resourceList)
        {
            DetailTile = 3;
            TileSize = 17;
            MaxPixelError = 8;
            ScaleX = 1;
            ScaleY = 1;
            ScaleZ = 1;
            MaxMipMapLevel = 5;
            VertexNormals = "no";

            this.ResourceList = resourceList;
        }

        public void LoadFromXml(string FileAddress)
        {
            XmlDocument TerrainXML = new XmlDocument();
            TerrainXML.Load(FileAddress);

            foreach (XmlNode sNode in TerrainXML.DocumentElement.ChildNodes)
            {
                switch (sNode.Name)
                {
                    case "Terrain":
                        Terrain = sNode.InnerText;
                        break;

                    case "WorldTexture":
                        WorldTexture = sNode.InnerText;
                        break;

                    case "DetailTexture":
                        DetailTexture = sNode.InnerText;
                        break;

                    case "DetailTile":
                        DetailTile = int.Parse(sNode.InnerText);
                        break;

                    case "TileSize":
                        TileSize = int.Parse(sNode.InnerText);
                        break;

                    case "PageSize":
                        PageSize = long.Parse(sNode.InnerText);
                        break;

                    case "MaxHeight":
                        MaxHeight = int.Parse(sNode.InnerText);
                        break;

                    case "MaxPixelError":
                        MaxPixelError = int.Parse(sNode.InnerText);
                        break;

                    case "MaxMipMapLevel":
                        MaxMipMapLevel = int.Parse(sNode.InnerText);
                        break;

                    case "PageWorldX":
                        PageWorldX = long.Parse(sNode.InnerText);
                        break;

                    case "PageWorldZ":
                        PageWorldZ = long.Parse(sNode.InnerText);
                        break;

                    case "VertexColours":
                        VertexColours = sNode.InnerText;
                        break;

                    case "UseTriStrips":
                        UseTriStrips = sNode.InnerText;
                        break;

                    case "VertexProgramMorph":
                        VertexProgramMorph = sNode.InnerText;
                        break;

                    case "VertexNormals":
                        VertexNormals = sNode.InnerText;
                        break;

                    case "LODMorphStart":
                        LODMorphStart = float.Parse(sNode.InnerText);
                        break;

                    case "ScaleX":
                        ScaleX = float.Parse(sNode.InnerText);
                        break;

                    case "ScaleY":
                        ScaleY = float.Parse(sNode.InnerText);
                        break;

                    case "ScaleZ":
                        ScaleZ = float.Parse(sNode.InnerText);
                        break;

                    case "MaterialName":
                        MaterialName = sNode.InnerText;
                        break;

                    case "MorphLODFactorParamName":
                        MorphLODFactorParamName = sNode.InnerText;
                        break;

                    case "MorphLODFactorParamIndex":
                        MorphLODFactorParamIndex = float.Parse(sNode.InnerText);
                        break;

                    case "PageSource":
                        PageSource = sNode.InnerText;
                        break;
                }
            }
        }

        /// <summary>
        /// Saves the data of this object in a readable format for TerrainSceneManager. The files used in this Terrain
        /// Config file most be copied in Resources folder out of this function. It will not manage it.
        /// </summary>
        /// <param name="FileAddress"></param>
        public void SaveToXml(string FileAddress)
        {
            XmlWriter terrainXml = XmlWriter.Create(FileAddress);

            terrainXml.WriteStartDocument();
            terrainXml.WriteStartElement("TerrainConfig");

            terrainXml.WriteElementString("Terrain", Path.GetFileName(this.Terrain));
            terrainXml.WriteElementString("WorldTexture", Path.GetFileName(this.WorldTexture));
            terrainXml.WriteElementString("DetailTexture", Path.GetFileName(this.DetailTexture));
            terrainXml.WriteElementString("DetailTile", this.DetailTile.ToString());
            terrainXml.WriteElementString("TileSize", this.TileSize.ToString());
            terrainXml.WriteElementString("PageSize", this.PageSize.ToString());
            terrainXml.WriteElementString("MaxHeight", this.MaxHeight.ToString());
            terrainXml.WriteElementString("MaxPixelError", this.MaxPixelError.ToString());
            terrainXml.WriteElementString("MaxMipMapLevel", this.MaxMipMapLevel.ToString());
            terrainXml.WriteElementString("PageWorldX", this.PageWorldX.ToString());
            terrainXml.WriteElementString("PageWorldZ", this.PageWorldZ.ToString());
            terrainXml.WriteElementString("VertexColours", this.VertexColours);
            terrainXml.WriteElementString("UseTriStrips", this.UseTriStrips);
            terrainXml.WriteElementString("VertexProgramMorph", this.VertexProgramMorph);
            terrainXml.WriteElementString("VertexNormals", this.VertexNormals);
            terrainXml.WriteElementString("LODMorphStart", this.LODMorphStart.ToString());
            terrainXml.WriteElementString("ScaleX", this.ScaleX.ToString());
            terrainXml.WriteElementString("ScaleY", this.ScaleY.ToString());
            terrainXml.WriteElementString("ScaleZ", this.ScaleZ.ToString());
            terrainXml.WriteElementString("MaterialName", this.MaterialName);
            terrainXml.WriteElementString("MorphLODFactorParamName", this.MorphLODFactorParamName);
            terrainXml.WriteElementString("MorphLODFactorParamIndex", this.MorphLODFactorParamIndex.ToString());
            terrainXml.WriteElementString("PageSource", this.PageSource);

            terrainXml.WriteEndElement();
            terrainXml.WriteEndDocument();
            terrainXml.Close();

        }

        /// <summary>
        /// Will return all of the properties that are file names.
        /// </summary>
        public static PropertyDescriptorCollection GetFileProperties()
        {
            Attribute[] att = new Attribute[1];
            att[0] = new EditorAttribute(typeof(GameDesigner.TypeEditors.OpenFileTypeEditor), typeof(System.Drawing.Design.UITypeEditor));

            return TypeDescriptor.GetProperties(typeof(TerrainSceneManagerTerrainConfig), att);
        }
    }

    /// <summary>
    /// Extends the <see cref="WireBoundingBox"/> class with the ability to set color for the rendered box.
    /// </summary>
    /// <remarks>
    /// Use <see cref="SceneNode.WireBoundingBox"/> property and populate it with an instance of this class.
    /// This class uses vertex colors to specify the color of the box. Depending on the material
    /// used in the base's <see cref="SimpleRenderable.Material"/>, the vertex colors may but need not to apply.
    /// The hereby approach is useful when using a material that doesn't receive light.
    /// </remarks>
    public class ColorWireBoundingBox : Axiom.Core.WireBoundingBox
    {
        #region Constructor

        /// <summary>
        /// Default constructor. Creates a bounding box in the <see cref="DefaultColor"/> color.
        /// </summary>
        public ColorWireBoundingBox()
            : this(DefaultColor)
        {
        }

        /// <summary>
        /// Creates a colored bounding box by extending the vertex data created in the base <see cref="WireBoundingBox"/> constructor 
        /// with a diffuse color buffer and populates it with the specified color.
        /// </summary>
        /// <param name="color"></param>
        public ColorWireBoundingBox(Axiom.Core.ColorEx color)
            : base()
        {
            short bindIndex = vertexData.vertexBufferBinding.NextIndex;

            // update vertex declaration
            vertexData.vertexDeclaration.AddElement(bindIndex, 0, VertexElementType.Color, VertexElementSemantic.Diffuse);

            // create a vertex buffer to hold the color values
            colorBuffer = HardwareBufferManager.Instance.CreateVertexBuffer(
                vertexData.vertexDeclaration.GetVertexSize(bindIndex),
                vertexData.vertexCount,
                BufferUsage.StaticWriteOnly);

            // bind the buffer to vertex data
            vertexData.vertexBufferBinding.SetBinding(bindIndex, colorBuffer);

            // set default color
            _color = color;
            SetVertexColors(_color);
        }

        #endregion

        #region Static

        public static Axiom.Core.ColorEx DefaultColor = ColorEx.White;

        #endregion

        #region Fields

        protected Axiom.Graphics.HardwareVertexBuffer colorBuffer;

        #endregion

        #region Properties

        protected Axiom.Core.ColorEx _color;
        /// <summary>
        /// Color of the bounding box
        /// </summary>
        public Axiom.Core.ColorEx Color
        {
            get
            {
                return _color;
            }
            set
            {
                if (value != _color)
                {
                    _color = value;
                    SetVertexColors(value);
                }
            }
        }

        #endregion

        #region Methods

        protected virtual void SetVertexColors(Axiom.Core.ColorEx color)
        {
            // convert color
            int c = Root.Instance.ConvertColor(color);

            // lock and fill the buffer
            IntPtr pbuf = colorBuffer.Lock(BufferLocking.Normal);

            unsafe
            {
                System.Int32* ppos = (System.Int32*)pbuf;

                for (int i = 0; i < vertexData.vertexCount; i++)
                {
                    ppos[i] = c;
                }
            }

            colorBuffer.Unlock();
        }

        #endregion
    }
}
